-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local file_sec = require 'utils.file'
local vos = require 'utils.vos'
local utils = require 'mc.utils'
local log = require 'mc.logging'
local task_mgmt = require 'mc.mdb.task_mgmt'
local NVME_MI_OPTIONS = require 'nvme.nvme_mi_protocol.nvme_mi_def'
local nvme_utils = require 'nvme.utils'
local task_state = task_mgmt.state
local task_status = task_mgmt.status

local LOG_PATH = '/tmp/nvme_telemetry_log'
local LOG_PATH_SOURCE = '/dev/shm/storage/tmp/nvme_telemetry_log'
local LOG_DIR_PATH = '/dev/shm/storage/tmp'

local nvme_admin_command = {}

-- 通过ADMIN COMMAND GET LOG PAGE(opcode:0xef)获取hw defiend smart log
function nvme_admin_command.get_hw_defined_smart_log(obj, uuid_index)
    local log_page = nvme_admin_command.get_log_page(obj, {nsid = NVME_MI_OPTIONS.NVME_NSID_ALL,
        log_id = NVME_MI_OPTIONS.LOG_PAGE_IDENTIFIERS.HW_DEFINED_SMART_LOG,
        data_len = NVME_MI_OPTIONS.DATA_LENGTH.HW_DEFINED_SMART_LOG, uuid_idx = uuid_index and uuid_index or 0})
    if not log_page then
        log:info('get Disk%s hw_defined smart log failed', obj.nvme.Slot)
        return
    end
    local parse_ret = NVME_MI_OPTIONS.hw_defined_smart_log_response:unpack(log_page, true)
    local magic_header = NVME_MI_OPTIONS.hw_defined_smart_log_magic_header:unpack(parse_ret.magic_header, true)

    if magic_header.identifier ~= NVME_MI_OPTIONS.HW_DEFINED_MAGIC then
        log:info('check Disk%s hw_defined smart log magic header failed', obj.nvme.Slot)
        return
    end
    log:info('get Disk%s hw_defined smart log successfully', obj.nvme.Slot)
    return parse_ret.attr_list
end

-- 通过ADMIN COMMAND GET LOG PAGE(opcode:0x05)获取comands support and effects
function nvme_admin_command.get_support_effects_log(obj)
    local log_page = nvme_admin_command.get_log_page(obj, {nsid = NVME_MI_OPTIONS.NVME_NSID_ALL,
        log_id = NVME_MI_OPTIONS.LOG_PAGE_IDENTIFIERS.COMMANDS_SUPPORTED_AND_EFFECTS,
        data_len = NVME_MI_OPTIONS.DATA_LENGTH.SUPPORT_EFFECTS_LOG
    })
    log:info('get Disk%s support and effects log %s', obj.nvme.Slot, log_page and 'successfully' or 'failed')
    return log_page
end

-- ADMIN COMMAND:GET LOG PAGE
function nvme_admin_command.get_log_page(obj, input)
    if not input or not next(input) then
        return
    end
    local log_page = nil
    local log_para = utils.table_copy(NVME_MI_OPTIONS.LOG_PAGE_PARAM)
    for k, v in pairs(input) do
        if log_para[k] then
            log_para[k] = v
        end
    end
    local numd = (log_para.data_len >> 2) - 1
    local data = NVME_MI_OPTIONS.log_request:pack({
        opcode = NVME_MI_OPTIONS.NVME_ADMIN_COMMANDS_OPCODE.NVME_ADMIN_GET_LOG_PAGE,
        flags = 0x3,
        ctrl_id = obj.ctrl_id,
        nsid = log_para.nsid,
        data_len = log_para.data_len,
        data_offset = log_para.data_offset,
        cdw10 = log_para.log_id |
            ((numd & 0xffff) << 16) | (log_para.rae and 1 << 15 or 0) | log_para.lsp << 8,
        cdw11 = (numd >> 16) | (log_para.lsi << 16),
        cdw12 = log_para.lpo & 0xffffffff,
        cdw13 = log_para.lpo >> 32,
        cdw14 = log_para.uuid_idx | (log_para.ot and 1 << 23 or 0) | log_para.csi << 24
    })
    obj.queue(function ()
        pcall(function ()
            log_page = obj.nvme_mi_obj:GetLogPage({data = data}):value()
        end)
    end)
    return log_page
end

-- 通过ADMIN COMMAND IDENTIFY(Opcode:x006 CSN0x11)获取info
function nvme_admin_command.identify_uuid_list(obj)
    local list = nvme_admin_command.identify(obj, {
        cdw10 = NVME_MI_OPTIONS.CNS_VALUES.ID_CNS_UUID_LIST})
    log:info('Identify Disk%s uuid list %s', obj.nvme.Slot, list and 'successfully' or 'failed')
    return list
end

-- 通过ADMIN COMMAND IDENTIFY(Opcode:x006 CSN0x01)获取info
function nvme_admin_command.identify_ctrl(obj)
    local list = nvme_admin_command.identify(obj, {
        cdw10 = NVME_MI_OPTIONS.CNS_VALUES.ID_CNS_CTRL})
    log:info('Identify Disk%s controller data structure %s', obj.nvme.Slot, list and 'successfully' or 'failed')
    return list
end

function nvme_admin_command.identify(obj, input)
    if not input or not next(input) then
        return
    end
    local list = nil
    local identify_para = utils.table_copy(NVME_MI_OPTIONS.IDENTIFY_PARAM)
    for k, v in pairs(input) do
        if identify_para[k] then
            identify_para[k] = v
        end
    end

    local data = NVME_MI_OPTIONS.log_request:pack({
        opcode = NVME_MI_OPTIONS.NVME_ADMIN_COMMANDS_OPCODE.NVME_ADMIN_IDENTIFY,
        flags = 0x3,
        ctrl_id = obj.ctrl_id,
        nsid = identify_para.nsid,
        data_len = NVME_MI_OPTIONS.DATA_LENGTH.IDENTIFY,
        data_offset = 0,
        cdw10 = identify_para.cdw10,
        cdw11 = identify_para.cdw11,
        cdw12 = identify_para.cdw12,
        cdw13 = identify_para.cdw13,
        cdw14 = identify_para.cdw14
    })

    obj.queue(function ()
        pcall(function ()
            list = obj.nvme_mi_obj:Identify({data = data}):value()
        end)
    end)
    return list
end

-- -- 通过ADMIN COMMAND GET LOG PAGE(opcode:0x02)获取SMART/health info
function nvme_admin_command.update_smart_log(obj)
    local smart_log = nvme_admin_command.get_log_page(obj, {nsid = NVME_MI_OPTIONS.NVME_NSID_ALL,
        log_id = NVME_MI_OPTIONS.LOG_PAGE_IDENTIFIERS.SMART_INFORMATION,
        data_len = NVME_MI_OPTIONS.DATA_LENGTH.SMART_LOG})
    if not smart_log then
        log:info('get Disk%s smart log failed', obj.nvme.Slot)
        obj.smart_log = nil
        return
    end

    local rsp = NVME_MI_OPTIONS.smart_log_response:unpack(smart_log, true)
    log:info('get Disk%s smart log identifiers successfully', obj.nvme.Slot)
    obj.smart_log = {
        critical_warning = rsp.critical_warning,
        temperature = rsp.temperature,
        avail_spare = rsp.avail_spare,
        spare_thresh = rsp.spare_thresh,
        percent_used = rsp.percent_used,
        endu_grp_crit_warn_sumry = rsp.endu_grp_crit_warn_sumry,
        data_units_read = rsp.data_units_read_h << 8 | rsp.data_units_read_l,
        data_units_written_l = rsp.data_units_written_l,
        data_units_written_h = rsp.data_units_written_h,
        host_reads = rsp.host_reads_h << 8 | rsp.host_reads_l,
        host_writes = rsp.host_writes_h << 8 | rsp.host_writes_l,
        ctrl_busy_time = rsp.ctrl_busy_time_h << 8 | rsp.ctrl_busy_time_l,
        power_cycles = rsp.power_cycles_h << 8 | rsp.power_cycles_l,
        power_on_hours = rsp.power_on_hours_l,
        unsafe_shutdowns = rsp.unsafe_shutdowns_h << 8 | rsp.unsafe_shutdowns_l,
        media_errors = rsp.media_errors_h << 8 | rsp.media_errors_l,
        num_err_log_entries = rsp.num_err_log_entries_h << 8 | rsp.num_err_log_entries_l,
        warning_temp_time = rsp.warning_temp_time,
        critical_comp_time = rsp.critical_comp_time,
        temp_sensor1 = rsp.temp_sensor1,
        temp_sensor2 = rsp.temp_sensor2,
        temp_sensor3 = rsp.temp_sensor3,
        temp_sensor4 = rsp.temp_sensor4,
        temp_sensor5 = rsp.temp_sensor5,
        temp_sensor6 = rsp.temp_sensor6,
        temp_sensor7 = rsp.temp_sensor7,
        temp_sensor8 = rsp.temp_sensor8,
        thm_temp1_trans_count = rsp.thm_temp1_trans_count,
        thm_temp2_trans_count = rsp.thm_temp2_trans_count,
        thm_temp1_total_time = rsp.thm_temp1_total_time,
        thm_temp2_total_time = rsp.thm_temp2_total_time
    }
end

function nvme_admin_command.get_add_smart_log(obj, vendor)
    local info = {
        lpo = 0,
        nsid = NVME_MI_OPTIONS.NVME_NSID_ALL,
        uuid_ix = 0,
        csi = 0,
        data_len = NVME_MI_OPTIONS.BUFFER_SIZE_512,
        lsi = 0,
        ot = false,
        rae = false,
        numd = (NVME_MI_OPTIONS.BUFFER_SIZE_512 >> 2) - 1,
        numdu = ((NVME_MI_OPTIONS.BUFFER_SIZE_512 >> 2) - 1) >> 16,
        numdl = ((NVME_MI_OPTIONS.BUFFER_SIZE_512 >> 2) - 1) & 0xffff
    }
    local ret
    local input = {
        message_type = NVME_MI_OPTIONS.NVME_MI_MESSAGE_TYPE.NVME_ADMIN_COMMAND,
        command_slot_identifier = 0,
        data = NVME_MI_OPTIONS.log_request:pack({
            opcode = NVME_MI_OPTIONS.NVME_ADMIN_COMMANDS_OPCODE.NVME_ADMIN_GET_LOG_PAGE,
            flags = 0x3,
            ctrl_id = 1,
            nsid = info.nsid,
            data_len = info.data_len,
            data_offset = 0,
            cdw10 = vendor | (info.numdl << 16) |
                (info.rae and 1 << 15 or 0) | NVME_MI_OPTIONS.NVME_NO_LOG_LSP << 8,
            cdw11 = info.numdu | (info.lsi << 16),
            cdw12 = info.lpo & 0xffffffff,
            cdw13 = info.lpo >> 32,
            cdw14 = info.uuid_ix | (info.ot and 1 << 23 or 0) | info.csi << 24
        })
    }
    obj.nvme_mi_mctp_obj.queue(function ()
        pcall(function ()
            ret = obj.nvme_mi_mctp_obj.nvme_mi_obj:RawRequest(input):value()
        end)
    end)
    return ret
end

local function update_feature_identifiers(obj)
    local feature_identifiers
    obj.queue(function ()
        pcall(function ()
            feature_identifiers = obj.nvme_mi_obj:FeatureIdentifiers():value()
        end)
    end)
    if not feature_identifiers or not next(feature_identifiers) then
        log:error('get Disk%s feature identifiers failed', obj.nvme.Slot)
        return
    end

    log:info('get Disk%s feature identifiers successfully', obj.nvme.Slot)
    obj.feature_identifiers = feature_identifiers
end

local function update_fw_log(obj)
    local fw_log
    obj.queue(function ()
        pcall(function ()
            fw_log = obj.nvme_mi_obj:FwLog():value()
        end)
    end)
    if not fw_log or not next(fw_log) then
        log:error('get Disk%s fw_log failed', obj.nvme.Slot)
        return
    end

    log:info('get Disk%s fw_log successfully,', obj.nvme.Slot)
    obj.fw_log = fw_log
end

local function update_supported_log_pages(obj)
    local supported_log_pages
    obj.queue(function ()
        pcall(function ()
            supported_log_pages = obj.nvme_mi_obj:SupportedLogPages():value()
        end)
    end)
    if not supported_log_pages or not next(supported_log_pages) then
        log:error('get Disk%s supported_log_pages failed', obj.nvme.Slot)
        return
    end

    log:info('get Disk%s supported_log_pages successfully', obj.nvme.Slot)
    obj.supported_log_pages = supported_log_pages
end

local function update_error_log(obj)
    local error_log
    obj.queue(function ()
        pcall(function ()
            error_log = obj.nvme_mi_obj:ErrorInformation():value()
        end)
    end)
    if not error_log or not next(error_log) then
        log:error('get Disk%s error_log failed', obj.nvme.Slot)
        return
    end

    log:info('get Disk%s error_log successfully, %s', obj.nvme.Slot)
    obj.error_log = error_log
end

function nvme_admin_command.update_admin_command_info(obj)
    nvme_admin_command.update_smart_log(obj)
    update_fw_log(obj)
    update_feature_identifiers(obj)
    update_error_log(obj)
    update_supported_log_pages(obj)
end

function nvme_admin_command.get_identify_data(obj, cns_value)
    local ret
    obj.nvme_mi_mctp_obj.queue(function ()
        pcall(function ()
            ret = obj.nvme_mi_mctp_obj.nvme_mi_obj:IdentifyNS():value()
        end)
    end)
    if not ret then
        log:error('get Disk%s IdentifyNS failed', obj.Slot)
        return
    end
    local rsp = nvme_utils.little_endian_table(ret)
    if not next(rsp) then
        return
    end
    return rsp
end

local function get_telemetry_data_size(obj, telemetry_log_para)
    telemetry_log_para.lpo = 0
    telemetry_log_para.data_len = NVME_MI_OPTIONS.BUFFER_SIZE_512
    telemetry_log_para.numd = (NVME_MI_OPTIONS.BUFFER_SIZE_512 >> 2) - 1
    telemetry_log_para.numdu = ((NVME_MI_OPTIONS.BUFFER_SIZE_512 >> 2) - 1) >> 16
    telemetry_log_para.numdl = ((NVME_MI_OPTIONS.BUFFER_SIZE_512 >> 2) - 1) & 0xffff
    local ret
    local input = {
        message_type = NVME_MI_OPTIONS.NVME_MI_MESSAGE_TYPE.NVME_ADMIN_COMMAND,
        command_slot_identifier = 0,
        data = NVME_MI_OPTIONS.log_request:pack({
            opcode = NVME_MI_OPTIONS.NVME_ADMIN_COMMANDS_OPCODE.NVME_ADMIN_GET_LOG_PAGE,
            flags = 0x3,
            ctrl_id = 1,
            nsid = telemetry_log_para.nsid,
            data_len = telemetry_log_para.data_len,
            data_offset = 0,
            cdw10 = telemetry_log_para.telemetry_type | (telemetry_log_para.numdl << 16) |
                (telemetry_log_para.rae and 1 << 15 or 0) | telemetry_log_para.lsp << 8,
            cdw11 = telemetry_log_para.numdu | (telemetry_log_para.lsi << 16),
            cdw12 = telemetry_log_para.lpo & 0xffffffff,
            cdw13 = telemetry_log_para.lpo >> 32,
            cdw14 = telemetry_log_para.uuid_ix | (telemetry_log_para.ot and 1 << 23 or 0) |
                telemetry_log_para.csi << 24
        })
    }
    obj.nvme_mi_mctp_obj.queue(function ()
        pcall(function ()
            ret = obj.nvme_mi_mctp_obj.nvme_mi_obj:RawRequest(input):value()
        end)
    end)
    if not ret then
        log:error('Disk%s get_telemetry_data_size failed', obj.Slot)
        return
    end
    local data = nvme_utils.parse_rsp_status(ret)
    if not data then
        return
    end
    local rsp = NVME_MI_OPTIONS.telemetry_log_page_rsp:unpack(data, true)
    return rsp.dalb3 * NVME_MI_OPTIONS.BUFFER_SIZE_512 + NVME_MI_OPTIONS.BUFFER_SIZE_512
end

local function get_telemetry_data(obj, telemetry_log_para, offset)
    telemetry_log_para.lpo = offset
    local ret
    local input = {
        message_type = NVME_MI_OPTIONS.NVME_MI_MESSAGE_TYPE.NVME_ADMIN_COMMAND,
        command_slot_identifier = 0,
        data = NVME_MI_OPTIONS.log_request:pack({
            opcode = NVME_MI_OPTIONS.NVME_ADMIN_COMMANDS_OPCODE.NVME_ADMIN_GET_LOG_PAGE,
            flags = 0x3,
            ctrl_id = 1,
            nsid = telemetry_log_para.nsid,
            data_len = telemetry_log_para.data_len,
            data_offset = 0,
            cdw10 = telemetry_log_para.telemetry_type | (telemetry_log_para.numdl << 16) |
                (telemetry_log_para.rae and 1 << 15 or 0) | telemetry_log_para.lsp << 8,
            cdw11 = telemetry_log_para.numdu | (telemetry_log_para.lsi << 16),
            cdw12 = telemetry_log_para.lpo & 0xffffffff,
            cdw13 = telemetry_log_para.lpo >> 32,
            cdw14 = telemetry_log_para.uuid_ix | (telemetry_log_para.ot and 1 << 23 or 0) |
                telemetry_log_para.csi << 24
        })
    }
    ret = obj.nvme_mi_mctp_obj.nvme_mi_obj:RawRequest(input):value()
    if not ret then
        -- 此帧获取异常则获取下一帧数据
        log:info('Disk%s get_telemetry_data failed, offset: %s', obj.Slot, offset)
        return ''
    end
    local data = nvme_utils.parse_rsp_status(ret)
    if not data then
        return ''
    end
    local rsp = NVME_MI_OPTIONS.log_response:unpack(data, true)
    return rsp.data
end

function nvme_admin_command.get_telemetry_log(obj, data_type, task_id)
    local telemetry_log_para = {
        nsid = NVME_MI_OPTIONS.NVME_NSID_ALL,
        uuid_ix = 0,
        csi = 0,
        lsi = 0,
        ot = false,
        rae = true,
        lsp = data_type == 'Controller' and NVME_MI_OPTIONS.NVME_NO_LOG_LSP or NVME_MI_OPTIONS.NVME_LSP_CREATE,
        telemetry_type = data_type == 'Controller' and
            NVME_MI_OPTIONS.LOG_PAGE_IDENTIFIERS.TELEMETRY_CONTROLLER_INITIATED or
            NVME_MI_OPTIONS.LOG_PAGE_IDENTIFIERS.TELEMETRY_HOST_INITIATED
    }
    local full_size = get_telemetry_data_size(obj, telemetry_log_para)
    if not full_size then
        nvme_utils.update_task(task_id, nil, task_state.Exception, task_status.Error)
        return
    end
    log:notice('get telemetry_log full_size: %s', full_size)
    if file_sec.check_real_path_s(LOG_DIR_PATH) ~= 0 then
        utils.mkdir_with_parents(LOG_DIR_PATH, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    end
    if vos.get_file_accessible(LOG_PATH_SOURCE) then
        utils.remove_file(LOG_PATH_SOURCE)
    end
    
    local fp, err = file_sec.open_s(LOG_PATH_SOURCE, 'a')
    if not fp then
        log:error('open telemetry_log file failed, err: %s', err)
        nvme_utils.update_task(task_id, nil, task_state.Exception, task_status.Error)
        return
    end
    local ret, transfer_bs, progress
    local offset = NVME_MI_OPTIONS.BUFFER_SIZE_512
    telemetry_log_para.data_len = NVME_MI_OPTIONS.BUFFER_SIZE_4096
    telemetry_log_para.numd = (NVME_MI_OPTIONS.BUFFER_SIZE_4096 >> 2) - 1
    telemetry_log_para.numdu = ((NVME_MI_OPTIONS.BUFFER_SIZE_4096 >> 2) - 1) >> 16
    telemetry_log_para.numdl = ((NVME_MI_OPTIONS.BUFFER_SIZE_4096 >> 2) - 1) & 0xffff
    while offset < full_size do
        if not vos.get_file_accessible(LOG_PATH_SOURCE) then
            ret = false
            log:error('telemetry_log is not accessible')
            break
        end
        -- 计算本次获取大小
        transfer_bs = (NVME_MI_OPTIONS.BUFFER_SIZE_4096 < (full_size - offset)) and
            NVME_MI_OPTIONS.BUFFER_SIZE_4096 or (full_size - offset)
        ret = get_telemetry_data(obj, telemetry_log_para, offset)
        -- 写入文件
        fp:write(ret)
        offset = offset + transfer_bs
        -- 确保任务完成前最高只到99
        progress = (offset / full_size) * 100
        nvme_utils.update_task(task_id, progress > 99 and 99 or progress, task_state.Running, task_status.OK)
    end
    fp:close()
    if not ret then
        nvme_utils.update_task(task_id, nil, task_state.Exception, task_status.Error)
        return
    end
    utils.chmod(LOG_PATH_SOURCE, utils.S_IRUSR | utils.S_IWUSR)

    if vos.get_file_accessible(LOG_PATH) then
        utils.remove_file(LOG_PATH)
    end
    file_sec.open_s(LOG_PATH, 'a'):close()
    utils.move_file(LOG_PATH_SOURCE, LOG_PATH)
    nvme_utils.update_task(task_id, 100, task_state.Completed, task_status.OK)
end

return nvme_admin_command